﻿#region License
/* Copyright (C) 2009 Tim Coulter
 * 
 * This file is part of ExtremeML.
 * 
 * ExtremeML is free software: you can redistribute
 * it and/or modify it under the terms of the GNU General Public
 * License, as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
*/
#endregion

using System;
using ExtremeML.Core;

namespace ExtremeML.Spreadsheet.Address
{
	/// <summary>
	/// Encapsulates a pair of <see cref="CellAddress"/> that jointly represent a cell range.
	/// </summary>
	public class CellArea : IAddressDependency, IWrapper
	{
		#region Housekeeping

		/// <summary>
		/// Initializes a new instance of the <see cref="CellArea"/> class.
		/// </summary>
		internal CellArea()
		{
			BeginCell = new CellAddress();
			EndCell = new CellAddress();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CellArea"/> class.
		/// </summary>
		/// <param name="beginCell">A <see cref="CellAddress"/> that specifies the address of the cell in which the range begins.</param>
		/// <param name="endCell">A <see cref="CellAddress"/> that specifies the address of the cell in which the range ends.</param>
		internal CellArea(
			CellAddress beginCell,
			CellAddress endCell)
		{
			BeginCell = beginCell;
			EndCell = endCell;
		}

		#endregion
		#region Properties

		/// <summary>
		/// Gets a <see cref="CellAddress"/> that specifies the address of the cell in which the range begins.
		/// </summary>
		public CellAddress BeginCell { get; private set; }

		/// <summary>
		/// Gets a <see cref="CellAddress"/> that specifies the address of the cell in which the range ends.
		/// </summary>
		public CellAddress EndCell { get; private set; }

		/// <summary>
		/// Gets a value that indicates whether this cell area occupies a single cell.
		/// </summary>
		public bool IsSingleCell
		{
			get { return IsPopulated() && BeginCell == EndCell; }
		}

		/// <summary>
		/// Gets a <see cref="string"/> that specifies the pair of cell references that form this range..
		/// </summary>
		public string Range
		{
			get
			{
				if (IsPopulated())
				{
					return IsSingleCell ? BeginCell.Reference : BeginCell.Reference + ":" + EndCell.Reference;
				}
				else
				{
					return null;
				}
			}
		}

		/// <summary>
		/// Gets or sets whether this instance expands when row or column
		/// insertion occurs immediately below or to the right of it.
		/// </summary>
		/// <remarks>
		/// When this class is used to represent normal rectangular regions,
		/// such as those occupied by tables or named ranges, insertion of
		/// a row or column immediately below or to the left of it has no
		/// effect. However, certain cell areas, such as those targeted by
		/// conditional formatting definitions are required to expand when
		/// row or column insertion occurs immediately adjacent to them.
		/// </remarks>
		public bool EagerMode { get; internal set; }

		#endregion
		#region Interface Implementation
		#region IAddressDependency

		/// <summary>
		/// Accepts notification that the specified number of empty
		/// rows have been inserted before the specified row index.
		/// </summary>
		/// <param name="rowIndex">The zero-based index of the row that follows the insertion point.</param>
		/// <param name="rowCount">The number of rows inserted.</param>
		public void NotifyRowInsertion(
			int rowIndex,
			int rowCount)
		{
			if (rowIndex < 0) throw new ArgumentOutOfRangeException("rowIndex");
			if (rowCount < 0) throw new ArgumentOutOfRangeException("rowCount");

			if (IsPopulated())
			{
				var begin = BeginCell.RowIndex.Value;
				var end = EndCell.RowIndex.Value;

				if (begin >= rowIndex)
				{
					// Shift
					BeginCell.RowIndex += rowCount;
					EndCell.RowIndex += rowCount;
				}
				else if (end >= rowIndex - (EagerMode ? 1 : 0))
				{
					// Resize
					EndCell.RowIndex += rowCount;

					if (RowsInserted != null)
					{
						RowsInserted(this, new RangeResizeEventArgs(rowIndex - begin, rowCount));
					}
				}
			}
		}

		/// <summary>
		/// Accepts notification that the specified number of rows
		/// have been deleted, starting at the specified row index.
		/// </summary>
		/// <param name="rowIndex">The zero-based index of the first row deleted.</param>
		/// <param name="rowCount">The number of rows deleted.</param>
		public void NotifyRowDeletion(
			int rowIndex,
			int rowCount)
		{
			if (rowIndex < 0) throw new ArgumentOutOfRangeException("rowIndex");
			if (rowCount < 0) throw new ArgumentOutOfRangeException("rowCount");

			if (IsPopulated())
			{
				// For an explanation of the logic applied here, refer to
				// the "Excel Observations" section of the developer guide.
				var begin = BeginCell.RowIndex.Value;
				var end = EndCell.RowIndex.Value;
			
				if (begin >= rowIndex + rowCount)
				{
					// Shift
					BeginCell.RowIndex -= rowCount;
					EndCell.RowIndex -= rowCount;
				}
				else if (begin >= rowIndex && end >= rowIndex + rowCount)
				{
					// Crop upper
					EndCell.RowIndex -= rowCount;
					rowCount = rowIndex + rowCount - begin;
					BeginCell.RowIndex = rowIndex;

					if (RowsDeleted != null)
					{
						RowsDeleted(this, new RangeResizeEventArgs(0, rowCount));
					}
				}
				else if (begin < rowIndex && end >= rowIndex && end < rowIndex + rowCount)
				{
					// Crop lower
					rowCount = end - rowIndex + 1;
					EndCell.RowIndex = rowIndex - 1;

					if (RowsDeleted != null)
					{
						RowsDeleted(this, new RangeResizeEventArgs(rowIndex - begin, rowCount));
					}
				}
				else if (begin >= rowIndex && end < rowIndex + rowCount)
				{
					// Delete
					if (RangeDeleted != null)
					{
						RangeDeleted(this, EventArgs.Empty);
					}
				}
				else if (begin < rowIndex && end >= rowIndex + rowCount)
				{
					// Resize
					EndCell.RowIndex -= rowCount;

					if (RowsDeleted != null)
					{
						RowsDeleted(this, new RangeResizeEventArgs(rowIndex - begin, rowCount));
					}
				}
			}
		}

		/// <summary>
		/// Accepts notification that the specified number of empty
		/// columns have been inserted before the specified column index.
		/// </summary>
		/// <param name="columnIndex">The zero-based index of the column that follows the insertion point.</param>
		/// <param name="columnCount">The number of columns inserted.</param>
		public void NotifyColumnInsertion(
			int columnIndex,
			int columnCount)
		{
			if (columnIndex < 0) throw new ArgumentOutOfRangeException("columnIndex");
			if (columnCount < 0) throw new ArgumentOutOfRangeException("columnCount");

			if (IsPopulated())
			{
				var begin = BeginCell.ColumnIndex.Value;
				var end = EndCell.ColumnIndex.Value;

				if (begin >= columnIndex)
				{
					// Shift
					BeginCell.ColumnIndex += columnCount;
					EndCell.ColumnIndex += columnCount;
				}
				else if (end >= columnIndex - (EagerMode ? 1 : 0))
				{
					// Resize
					EndCell.ColumnIndex += columnCount;

					if (ColumnsInserted != null)
					{
						ColumnsInserted(this, new RangeResizeEventArgs(columnIndex - begin, columnCount));
					}
				}
			}
		}

		/// <summary>
		/// Accepts notification that the specified number of columns
		/// have been deleted, starting at the specified column index.
		/// </summary>
		/// <param name="columnIndex">The zero-based index of the first column deleted.</param>
		/// <param name="columnCount">The number of columns deleted.</param>
		public void NotifyColumnDeletion(
			int columnIndex,
			int columnCount)
		{
			if (columnIndex < 0) throw new ArgumentOutOfRangeException("columnIndex");
			if (columnCount < 0) throw new ArgumentOutOfRangeException("columnCount");

			if (IsPopulated())
			{
				// For an explanation of the logic applied here, refer to
				// the "Excel Observations" section of the developer guide.
				var begin = BeginCell.ColumnIndex.Value;
				var end = EndCell.ColumnIndex.Value;

				if (begin >= columnIndex + columnCount)
				{
					// Shift
					BeginCell.ColumnIndex -= columnCount;
					EndCell.ColumnIndex -= columnCount;
				}
				else if (begin >= columnIndex && end >= columnIndex + columnCount)
				{
					// Crop left
					EndCell.ColumnIndex -= columnCount;
					columnCount = columnIndex + columnCount - begin;
					BeginCell.ColumnIndex = columnIndex;

					if (ColumnsDeleted != null)
					{
						ColumnsDeleted(this, new RangeResizeEventArgs(0, columnCount));
					}
				}
				else if (begin < columnIndex && end >= columnIndex && end < columnIndex + columnCount)
				{
					// Crop right
					columnCount = end - columnIndex + 1;
					EndCell.ColumnIndex = columnIndex - 1;

					if (ColumnsDeleted != null)
					{
						ColumnsDeleted(this, new RangeResizeEventArgs(columnIndex - begin, columnCount));
					}
				}
				else if (begin >= columnIndex && end < columnIndex + columnCount)
				{
					// Delete
					if (RangeDeleted != null)
					{
						RangeDeleted(this, EventArgs.Empty);
					}
				}
				else if (begin < columnIndex && end >= columnIndex + columnCount)
				{
					// Resize
					EndCell.ColumnIndex -= columnCount;

					if (ColumnsDeleted != null)
					{
						ColumnsDeleted(this, new RangeResizeEventArgs(columnIndex - begin, columnCount));
					}
				}
			}
		}

		#endregion
		#region IWrapper

		/// <summary>
		/// Returns a value that indicates whether this instance wraps content that should be saved to the document.
		/// </summary>
		public bool IsPopulated()
		{
			return BeginCell.IsPopulated() && EndCell.IsPopulated();
		}

		/// <summary>
		/// Establishes relationships with other package content objects.
		/// </summary>
		public void Activate()
		{
			// This class requires no activation.		
		}

		/// <summary>
		/// Safely terminates relationships with other package content objects.
		/// </summary>
		public void Deactivate()
		{
			// This class requires no deactivation.		
		}

		#endregion
		#endregion
		#region Overrides

		/// <summary>
		/// Overrides the base class to return the value of the <see cref="Range"/> property.
		/// </summary>
		/// <returns>A <see cref="string"/> that represents the value of this instance.</returns>
		public override string ToString()
		{
			return Range;
		}

		/// <summary>
		/// Overrides the equality operator to compare the values of two <see cref="CellArea"/> instances.
		/// </summary>
		/// <param name="a">A <see cref="CellArea"/> instance to compare.</param>
		/// <param name="b">A <see cref="CellArea"/> instance to compare.</param>
		/// <returns>A value that indicates whether the passed instances are equal.</returns>
		public static bool operator ==(
			CellArea a,
			CellArea b)
		{
			return
				a.IsPopulated() &&
				b.IsPopulated() &&
				a.BeginCell == b.BeginCell &&
				a.EndCell == b.EndCell;
		}

		/// <summary>
		/// Overrides the inequality operator to compare the values of two <see cref="CellArea"/> instances.
		/// </summary>
		/// <param name="a">A <see cref="CellArea"/> instance to compare.</param>
		/// <param name="b">A <see cref="CellArea"/> instance to compare.</param>
		/// <returns>A value that indicates whether the passed instances are not equal.</returns>
		public static bool operator !=(
			CellArea a,
			CellArea b)
		{
			return !(a == b);
		}

		#endregion
		#region Events

		/// <summary>
		/// Occurs when the row insertion changes the height of this range.
		/// </summary>
		internal event RangeResizeEventHandler RowsInserted;

		/// <summary>
		/// Occurs when the row deletion changes the height of this range.
		/// </summary>
		internal event RangeResizeEventHandler RowsDeleted;

		/// <summary>
		/// Occurs when the column insertion changes the width of this range.
		/// </summary>
		internal event RangeResizeEventHandler ColumnsInserted;

		/// <summary>
		/// Occurs when the column deletion changes the width of this range.
		/// </summary>
		internal event RangeResizeEventHandler ColumnsDeleted;

		/// <summary>
		/// Occurs when the this range is deleted by a worksheet row or column deletion operation.
		/// </summary>
		internal event EventHandler RangeDeleted;

		#endregion
	}
}
